package com.angle3d;

import java.util.ArrayList;
import java.util.List;

/**
 * Class representing a node of the 3D scene to be rendered.
 * The scene is organized in a hierarchical tree structure, called Scene graph, represented by one or more nodes.
 * Each node can have zero, one, or more children nodes.
 * A node can have at most one parent.
 * The root node of such scene graph is what we actually call "the scene".
 * 
 * @author Alessandro Boccalatte
 */
public class Node {

	public enum Reference
	{
		WORLD_REF,
		VIEW_REF,
	};
	
	private String mName = "";
	private List<SceneObject> mSceneObjects = new ArrayList<SceneObject>();
	private boolean mTransparent = false;
	private boolean mVisible = true;
	private boolean mUpdatable = true;
	private Matrix4 mLocalMatrix = Matrix4.identity();
	private Reference mReference = Reference.WORLD_REF;
	private Node mParent = null;
	private boolean mBillboard = false;
	
	private final List<Node> mChildren = new ArrayList<Node>();
	private final List<NodeUpdateListener> mUpdateListeners = new ArrayList<NodeUpdateListener>();
	private final List<NodeRenderListener> mRenderListeners = new ArrayList<NodeRenderListener>();
	
	/**
	 * Creates a Node with a given name.
	 * @param name the name of the node
	 */
	public Node(String name) {
		mName = name;
	}
	
	/**
	 * Creates a Node.
	 */
	public Node() {
		this("");
	}
	
	/**
	 * Sets the node name.
	 * @param name a name
	 * @see #getName()
	 */
	public void setName(String name) {
		mName = name;
	}
	
	/**
	 * Returns the node name.
	 * @return the node name.
	 * @see #setName(String)
	 */
	public String getName() {
		return mName;
	}
	
	/**
	 * Sets the node reference. This can be one of:
	 * - WORLD_REF (the default)
	 * - VIEW_REF
	 * If a node reference is WORLD_REF, the node position will be in world coordinates;
	 * if the reference is VIEW_REF, then the node position is expressed w.r.t. the camera reference frame.
	 * @param reference a reference
	 * @see #getReference()
	 */
	public void setReference(Reference reference) {
		mReference = reference;
	}
	
	/**
	 * Returns the node reference.
	 * @return the node reference.
	 * @see #setReference(Reference)
	 */
	public Reference getReference() {
		return mReference;
	}
	
	/**
	 * Tells whether this node is transparent.
	 * @return true if the node is transparent, false otherwise.
	 * @see #setTransparent(boolean)
	 */
	public boolean isTransparent() {
		return mTransparent;
	}
	
	/**
	 * Sets this node as transparent/opaque.
	 * @param transparent
	 * @see #isTransparent()
	 */
	public void setTransparent(boolean transparent) {
		mTransparent = transparent;
	}
	
	/**
	 * Tells whether this node is visible.
	 * @return true if the node is visible, false otherwise.
	 * @see #setVisible(boolean)
	 */
	public boolean isVisible() {
		return mVisible;
	}
	
	/**
	 * Sets this node as visible/invisible.
	 * @param visible
	 * @see #isVisible()
	 */
	public void setVisible(boolean visible) {
		mVisible = visible;
	}
	
	/**
	 * Tells whether this node is updatable, i.e. if it can be udpated.
	 * @return true if the node is updatable.
	 * @see #setUpdatable(boolean)
	 */
	public boolean isUpdatable() {
		return mUpdatable;
	}
	
	/**
	 * Sets this node as updatable/non-updatable
	 * @param updatable
	 * @see #isUpdatable()
	 */
	public void setUpdatable(boolean updatable) {
		mUpdatable = updatable;
	}
	
	/**
	 * Tells whether this node is billboarded or not.
	 * @return true if this node is billboarded
	 */
	public boolean isBillboard() {
		return mBillboard;
	}
	
	/**
	 * Sets this node as billboarded or not.
	 * @param billboard
	 */
	public void setBillboard(boolean billboard) {
		mBillboard = billboard;
	}
	
	/**
	 * Sets the local transformation matrix of the node.
	 * @param matrix a transformation matrix
	 */
	public void setLocalMatrix(Matrix4 matrix) {
		mLocalMatrix = matrix;
	}
	
	/**
	 * Returns the local transformation matrix of the node.
	 * @return the local transformation matrix of the node.
	 */
	public Matrix4 getLocalMatrix() {
		return mLocalMatrix;
	}
	
	/**
	 * Adds a {@link SceneObject} to this node, i.e. an entity to be rendered.
	 * @param object
	 * @see {@link SceneObject}
	 */
	public void addSceneObject(SceneObject object) {
		mSceneObjects.add(object);
	}
	
	/**
	 * Returns the {@link SceneObject} associated to this node at the given index.
	 * @return the {@link SceneObject} associated to this node at the given index.
	 * @see #addSceneObject(SceneObject)
	 */
	public SceneObject getSceneObject(int index) {
		return mSceneObjects.get(index);
	}
	
	/**
	 * Removes a {@link SceneObject} from this node.
	 * @param index the scene object location in the array of scene objects
	 */
	public void removeSceneObject(int index) {
		mSceneObjects.remove(index);
	}
	
	/**
	 * Removes a {@link SceneObject} from this node.
	 * @param sceneObject the scene object to remove
	 */
	public void removeSceneObject(SceneObject sceneObject) {
		mSceneObjects.remove(sceneObject);
	}
	
	/**
	 * Removes all the {@link SceneObject}s from this node.
	 */
	public void removeSceneObjects() {
		mSceneObjects.clear();
	}
	
	/**
	 * Adds a {@link NodeUpdateListener} to this node. The listener will be notified each time the node is updated.
	 * @param updateListener the listener to be added
	 */
	public void addUpdateListener(NodeUpdateListener updateListener) {
		mUpdateListeners.add(updateListener);
	}
	
	/**
	 * Removes a {@link NodeUpdateListener} from this node.
	 * @param updateListener the listener to be removed
	 */
	public void removeUpdateListener(NodeUpdateListener updateListener) {
		mUpdateListeners.remove(updateListener);
	}
	
	/**
	 * Removes all the NodeUpdateListeners from this node.
	 */
	public void removeUpdateListeners() {
		mUpdateListeners.clear();
	}
	
	/**
	 * Adds a {@link NodeRenderListener} to this node. The listener will be notified before and after the node is rendered.
	 * @param renderListener the listener to be added
	 */
	public void addRenderListener(NodeRenderListener renderListener) {
		mRenderListeners.add(renderListener);
	}
	
	/**
	 * Removes a {@link NodeRenderListener} from this node.
	 * @param renderListener the listener to be removed
	 */
	public void removeRenderListener(NodeRenderListener renderListener) {
		mRenderListeners.remove(renderListener);
	}
	
	/**
	 * Removes all the render listeners from this node.
	 */
	public void removeRenderListeners() {
		mRenderListeners.clear();
	}
	
	protected void fireUpdateEvent(float dt) {
		for (NodeUpdateListener listener : mUpdateListeners) {
			listener.onNodeUpdate(this, dt);
		}
	}
	
	protected void firePreRenderEvent(View3D view) {
		for (NodeRenderListener listener : mRenderListeners) {
			listener.onNodePreRender(this, view);
		}
	}
	
	protected void firePostRenderEvent(View3D view) {
		for (NodeRenderListener listener : mRenderListeners) {
			listener.onNodePostRender(this, view);
		}
	}
	
	/**
	 * Returns the number of child nodes.
	 * @return the number of child nodes.
	 */
	public int getChildCount() {
		return mChildren.size();
	}
	
	/**
	 * Returns the child node at the given index.
	 * @param index the index in the children array
	 * @return the child ndoe at the given index.
	 */
	public Node getChild(int index) {
		if (index < 0 || index >= mChildren.size()) return null;
		return mChildren.get(index);
	}
	
	/**
	 * Adds a child node to this node.
	 * @param child a node
	 */
	public void addChild(Node child) {
		mChildren.add(child);
		child.mParent = this;
	}
	
	/**
	 * Removes a child node.
	 * @param child a child node
	 */
	public void removeChild(Node child) {
		mChildren.remove(child);
		child.mParent = null;
	}
	
	/**
	 * Removes a node at a given index in the children array.
	 * @param index the index of the child node in the children array
	 */
	public void removeChild(int index) {
		Node child = mChildren.remove(index);
		child.mParent = null;
	}
	
	/**
	 * Removes all the child nodes.
	 */
	public void removeChildren() {
		for (Node child : mChildren) {
			child.mParent = null;
		}
		mChildren.clear();
	}
	
	/**
	 * Updates the node.
	 * @param dt the time elapsed (in seconds) since the last update pass.
	 */
	public void update(float dt) {
		for (SceneObject sceneObject : mSceneObjects) {
			updateSceneObject(sceneObject, dt);
		}
		fireUpdateEvent(dt);
	}
	
	protected void updateSceneObject(SceneObject sceneObject, float dt) {
		sceneObject.update(dt);
	}
	
	/**
	 * Renders this node.
	 * @param view the view in which the node must be rendered.
	 */
	public void render(View3D view) {
		firePreRenderEvent(view);
		for (SceneObject sceneObject : mSceneObjects) {
			renderSceneObject(sceneObject, view);
		}
		firePostRenderEvent(view);
	}
	
	protected void renderSceneObject(SceneObject sceneObject, View3D view) {
		Camera camera = view.getCamera();
		
		Matrix4 projection = camera.getProjection();
			
		Matrix4 modelview = Matrix4.identity();
		if (mReference == Reference.WORLD_REF) {
			modelview.postMultiply(camera.getModelView());
		}
		modelview.postMultiply(getGlobalMatrix(this));
		
		if (mBillboard) {
			modelview.setBillboarded();
		}
		
		sceneObject.render(view, projection, modelview);
	}
	
	/**
	 * Finds and returns a node by name; the search starts at a given root node
	 * and goes down recursively through the children.
	 * If no matching node is found, null is returned.
	 * @param root the root node for the search
	 * @param name the name to match
	 * @return a node matching the name, or null
	 */
	public static Node findNodeByName(Node root, String name) {
		if (root == null  || name == null) {
			return null;
		}
		if (name.equals(root.getName())) {
			return root;
		}
		for (int i = 0; i < root.getChildCount(); ++i) {
			Node childFound = findNodeByName(root.getChild(i), name);
			if (childFound != null) return childFound;
		}
		return null;
	}
	
	private static Matrix4 getGlobalMatrix(Node node) {
		if (node == null) return Matrix4.identity();
		
		Matrix4 result = new Matrix4(node.getLocalMatrix());
		if (node.mParent != null) {
			result.preMultiply(getGlobalMatrix(node.mParent));
		}
		return result;
	}
}
